int pre[N];
int rank1[N];
//rank[x]是在按秩合并时用的,rank[x]代表x这个元素在自己集合构成的森林中的层数。
//但是路径压缩会破坏树的结构，在一些情况下是不能使用的，按秩合并则不会

//初始化
//代表第i个元素需要初始化
void creat(int i)
{
	pre[i]=i;
	rank1[i]=0;
}

//按秩合并
void Union(int i,int j)
{
    i=find(i);
    j=find(j);
    if(i==j) return ;
    if(rank1[i]>rank1[j]) pre[j]=i;
    else
    {
        if(rank1[i]==rank1[j]) rank1[j]++;  
        pre[i]=j;
    }
}
auto find = [&](auto &&self,int x)
{
    if(pre[x] == x) return x;
    return pre[x] = self(self,pre[x]);
};
//递归版本路径压缩
auto merge = [&](int x,int y)
{
    size[find(find,x)] += size[find(find,y)];
    pre[find(find,y)] = find(find,x);
};


//路径压缩(非递归)
int find(int x)
{
    int k, j, r;
    r = x;
    while(r != pre[r])
        r = pre[r];
    k = x;        
    while(k != r) 
    {
        j = pre[k]; 
        pre[k] = r; 
        k = j; 
    }
    return r; 
}


//边带权并查集
/*用d[x]保存节点x到父节点fa[x]之间的边权。
在每次路径压缩后，每个访问过的节点都会直接指向树根，
如果我们同时更新这些节点的d值，就可以利用路径压缩过程来统计每个节点到树根之间的路径上的一些信息。
*/

int find(int x)
{
    if(p[x] != x)
    {
        int t = find(pre[x]);   
        dis[x] += dis[pre[x]];
        pre[x] = t;
    }
    return p[x];
}
void merge(int x,int y,int val)//将x合并到y
{
    int px = find(x);
    int py = find(y);
    if(px != py){
        p[px] = py;
        dis[px] = -dis[x] + dis[y] + val;
    }
}


int n, m;
int p[N], d[N];
int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);//因为这个路径的长度（高度），是需要自上而下加起来的，从根节点往下走
        // 然后层层调用上一次递归的结果，在这个过程中，d[p[p[p[...p[x]]]]]会不停更新，也就是+1+1...
        // 最后到最外面这层调用，d[x] += d[p[x]];的时候，d[p[x]]已经等于p[x]到根节点的距离了。
        d[x] += d[p[x]];// 更新距离
        p[x] = t;
    }
    return p[x];
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    int res = 0;
    while (m -- )
    {
        int t, x, y;
        scanf("%d%d%d", &t, &x, &y);
        if (x > n || y > n) res ++ ;
        else
        {
            int px = find(x), py = find(y);
            if (t == 1)
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] - d[x];//定义距离
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }
    printf("%d\n", res);
    return 0;
}

//扩展域并查集
#include<bits/stdc++.h>
using namespace std;
const int N = 2e4 + 10;
const int M = 1e5 + 10;
int pre[N * 2];
int find(int x)
{
    int k, j, r;
    r = x;
    while(r != pre[r])
        r = pre[r];
    k = x;        
    while(k != r) 
    {
        j = pre[k]; 
        pre[k] = r; 
        k = j; 
    }
    return r; 
}
struct Edge
{
    int a,b,w;
    bool operator < (const Edge & W) const
    {
        return w < W.w;
    }
}edge[M];
void creat(int i)
{
    pre[i]=i;
}
void solve()
{
    int n,m;
    cin >> n >> m;
    for(int i = 1; i <= n * 2; i++) creat(i);
    for(int i = 1; i <= m; i ++) cin >> edge[i].a >> edge[i].b >> edge[i].w;
    sort(edge + 1,edge + 1 + m);
    int ans = 0;
    for(int i = m; i >= 1; i --)
    {
        int a = edge[i].a;
        int b = edge[i].b;
        int w = edge[i].w;
        if(find(a) != find(b))
        {
            pre[find(a)] = find(b + n);
            pre[find(a + n)] = find(b);
        }
        else
        {
            ans = w;
            break;
        }
    }
    cout << ans << endl;
}